
module Scrum
	
	def Scrum.consolidate_status list
	   status_candidate = :done
		list.each{|item|
			if(item.status == :in_progress) then
				#if a single item is in progress the whole status is that one
				status_candidate = :in_progress
				break
			end 
			
			if(item.status == :todo) then
				#if a single item is in todo then the whole status can't be 'done'
				#but we need to keep looking for in progress item so don't break the loop yet
				status_candidate = :todo
			end
		}
		status_candidate
	end
	
	def Scrum.new_project project_name
       return Project.new    
	end
	
	class Task		
		
		attr_reader :id, :description
		
		def initialize story, description
		  @description=description
		  @story=story
		  @id="#{story.id}-#{description.hash}"
		  @status = :todo
		end
		
		def status
			@status
		end
		
		def begin_work!
			@status = :in_progress
			notify_story
		end
		
		def finished!
			@status = :done
			notify_story
		end
		
		def stopped_work!
			@status = :todo
			notify_story
		end
		
		
		private 
		
		#notifies the story that this task has changed its status
		def notify_story
			@story.be_aware_of_change_in self
		end
	end
	
	class Story
		
		attr_reader :description, :status, :sprint, :estimate
		
		def initialize id, description
		  @id=id
		  @status = :todo
		  @description=description
		  @tasks=[]
		end
		
		def id
			@id
		end
		
		def tasks
		  @tasks
		end
		
		def has_a_new_task task_description 
		  task = Task.new self, task_description
		  @tasks << task
		  task	
		end
		
		def has_removed_task description
			task_to_be_deleted = task description
			@tasks.reject!{|task| task.id==task_to_be_deleted.id}
		end
		
		#returns the task with the given description
		def task description
			@tasks.find{|task| task.description==description}
		end
		
		def be_aware_of_change_in task			
			@status = Scrum::consolidate_status @tasks
		end
		
		def sprint= other_sprint
			@sprint.remove_story self unless @sprint.nil?
			@sprint= other_sprint
		end
		
		def estimate_as story_points
			@estimate=story_points
		end
		
		def to_s
			"#{self.class} id: [#{@id}] description: [#{@description}] tasks: [#{@tasks}]"
		end
	end
	
	class Backlog
		def initialize
			@stories = {}
		end
		
        def add_a_story story_data
            raise ArgumentError.new "Invalid story data #{story_data.keys}" unless (story_data.key? 'id' and story_data.key? 'description')

            story_id = story_data['id']
            story_description=story_data['description'] 
            story = Story.new story_id, story_description
            @stories[story.id]=story
            story    
        end		
		
		def story story_id			   
			@stories[story_id]
		end
		
		#adds an existing story to the product backlog (removes it from sprints)
		def << story
			story.sprint = nil
			@stories[story.id]=story
		end

        def unscheduled
        	@stories.values.select{|story|  story.sprint.nil?   }
        end
		
        def scheduled
        	@stories.values.select{|story|  not story.sprint.nil?   }
        end		

        def estimated
        	@stories.values.select{|story|  not story.estimate.nil?   }
        end     

        def not_estimated
            @stories.values.select{|story|  story.estimate.nil? }
        end     
				
		def size
			@stories.size
		end
		
	end
	
	class Impediment
		
		attr_reader :description 
		
		def initialize description
			@description = description
		end
		
		def open?
			@status != :cleared
		end
		
		def clear!
			@status= :cleared
		end
	end
	
	class Member
		
		def initialize project
			@project=project
		end
		
		def started_working_on task
			task.begin_work!
		end
		
		def has_finished task
			task.finished! 
		end
		
		#reports an impediment at the current sprint
		def reports_impediment impediment_description
			@project.sprints.current.add_impediment Impediment.new impediment_description
		end
		
		def reports_cleared impediment
			@project.clear_impediment impediment
		end
		
		#the developer has stopped working on a given task
		def stopped_working_on task
			task.stopped_work!
		end
	end
	
	class Team
		
		def initialize project
			@project = project
		end
		
		def new_member name
			Member.new @project
		end
	end
	
	class Sprint
		
		attr_accessor :id, :goal, :stories
		
		def initialize project
			@project = project
			@impediments = []
			@stories = []
		end
		
		def add_impediment impediment
		  @project.add_impediment impediment
		  @impediments << impediment
			
		end
		
		def impediments
			@impediments
		end
		
		#gets a single impediment
		def impediment description
			@impediments.find{|impediment| impediment.description == description}
		end
		
		#adds a story to this sprint
		def << story
			story.sprint=self
			@stories  << story
		end
		
		#removes a story from this sprint
		def remove_story story_to_be_removed
			@stories.reject!{|story| story.id==story_to_be_removed.id} 
		end
		
		def to_s
			"#{self.class} goal [#{@goal}]"
		end

        def after? other
            my_order  > (@project.sprints.order_of other)
        end
		
        def before? other
            my_order  < (@project.sprints.order_of other)
        end
		
        def next
            @project.sprints[my_order() +1]
        end
		

        def previous
        	previous_sprint = @project.sprints[my_order() -1] unless my_order == 0
			previous_sprint
        end
		
		def status
			if(@stories.size==0) then
				:todo
			else
			 Scrum::consolidate_status @stories
			end
		end
		
		private
		
		def my_order
			(@project.sprints.order_of self)
		end
	end
	
	class SprintSchedule
		
		def initialize project
			@project = project
			
			default_sprint = Sprint.new project
			default_sprint.id="1"
			default_sprint.goal="First Sprint"
			@sprints = [default_sprint]
		end
		
		def add sprint
			@sprints << sprint
		end
		
		def remove sprint
			@sprints.reject!{|s| s == sprint}
		end
		
		def current
			@sprints.find{|sprint| not sprint.status == :done}
		end
		
		def first
			@sprints.first
		end
		
		def size
			@sprints.size
		end
		
		def include? sprint
			@sprints.include? sprint
		end
		
		def [] index
			@sprints[index]
		end
		
		#returns the position of the given sprint in the schedule
		def order_of sprint
			@sprints.index sprint
		end
		
		def to_s
			"#{self.class}"
		end
	end
	
	class Planner
		def initialize project
			@project=project
		end
		
		def schedule scheduling
			if (not scheduling.is_a? Hash) then 
				raise ArgumentException.new 'scheduling must be a map'
			end
				
			scheduling['at'] << scheduling['story']
		end
		
		def unschedule story
			@project.backlog << story			
		end
	end
	
	class Project
		
		attr_reader :team, :sprints, :backlog
		
		def initialize
			@backlog = Backlog.new
			@team=Team.new self
			@sprints = SprintSchedule.new self
			@all_impediments=[]
		end
		

        #defines a new scrum master for the project
        def scrum_master name
          Member.new self
        end
		
		def clear_impediment impediment_description
			impediment = @all_impediments.find{|impediment| impediment.description == impediment_description}
			impediment.clear! unless impediment.nil? 
		end
		
		
		def at_daily_scrum 		
			yield @backlog
		end
		
		def at_estimation_meeting
			yield @backlog
		end
		
		def at_sprint_planning_1
			yield @backlog, Planner.new(self)
		end
		
		def at_sprint_planning_2
			yield @sprints.current
		end
		
		def add_impediment impediment
			@all_impediments << impediment
		end
		
		#creates a new sprint
		def add_a_sprint sprint_data
			new_sprint = Sprint.new self
			new_sprint.goal = sprint_data['goal']
			@sprints.add new_sprint
			new_sprint			
		end
		
		def remove_sprint sprint
			#TODO: @backlog << sprint.stories should be tested
			@sprints.remove sprint			
		end
		
	end
end